# -*- coding: utf-8 -*-
"""
一键启动脚本（Python 版）
- 启动后端 Flask API（backend/run.py，端口 5000）
- 启动前端 Next.js Web（frontend，端口 3000）

使用方法：
  python start.py                # 启动（若缺少依赖会提示）
  python start.py --install      # 先安装后端/前端依赖，再启动
"""
import argparse
import os
import signal
import subprocess
import sys
import threading
import time
from pathlib import Path
from typing import Optional

ROOT = Path(__file__).resolve().parent
BACKEND_DIR = ROOT / "backend"
FRONTEND_DIR = ROOT / "frontend"


# 为了避免引入额外依赖，这里懒加载 shutil
try:
    import shutil  # noqa: E402
except Exception:  # pragma: no cover
    shutil = None  # type: ignore


def cmd_exists(cmd: str) -> bool:
    if shutil is None:
        return False
    return shutil.which(cmd) is not None


def get_npm_cmd():
    """Get the correct npm command for Windows"""
    if os.name == 'nt':  # Windows
        # Try npm.cmd first, then npm.exe, then npm
        for cmd in ['npm.cmd', 'npm.exe', 'npm']:
            if cmd_exists(cmd):
                return cmd
        return 'npm.cmd'  # fallback
    return 'npm'


def run_blocking(cmd, cwd: Optional[Path] = None):
    print(f"$ {' '.join(cmd)}  (cwd={cwd or ROOT})")
    subprocess.run(cmd, cwd=str(cwd or ROOT), check=True)


def stream_output(prefix: str, proc: subprocess.Popen):
    assert proc.stdout is not None
    assert proc.stderr is not None
    
    def process_stream(stream, is_error=False):
        error_prefix = "[ERR]" if is_error else ""
        for line in iter(stream.readline, b""):
            try:
                # Use errors='replace' to replace characters that cannot be decoded
                decoded_line = line.decode(errors='replace').rstrip()
                if decoded_line:  # Only output non-empty lines
                    output = f"[{prefix}]{error_prefix} {decoded_line}\n"
                    if is_error:
                        sys.stderr.write(output)
                        sys.stderr.flush()  # Flush output immediately
                    else:
                        sys.stdout.write(output)
                        sys.stdout.flush()  # Flush output immediately
            except Exception as e:
                sys.stderr.write(f"[{prefix}]{error_prefix} Decoding error: {str(e)}\n")
                sys.stderr.flush()
    
    # Create two threads to handle stdout and stderr separately
    stdout_thread = threading.Thread(target=process_stream, args=(proc.stdout, False), daemon=True)
    stderr_thread = threading.Thread(target=process_stream, args=(proc.stderr, True), daemon=True)
    
    stdout_thread.start()
    stderr_thread.start()


def start_process(cmd, cwd: Optional[Path], prefix: str) -> subprocess.Popen:
    print(f"Starting {prefix}: {' '.join(cmd)} (cwd={cwd})")
    try:
        # Use binary mode to avoid encoding issues
        proc = subprocess.Popen(
            cmd,
            cwd=str(cwd or ROOT),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            universal_newlines=False,
            bufsize=1,
            creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0,
        )
        # Process output streams directly
        stream_output(prefix, proc)
        # Check if process failed immediately
        if proc.poll() is not None:
            print(f"[{prefix}][ERROR] Process exited immediately, return code: {proc.returncode}")
            return None
        return proc
    except Exception as e:
        print(f"[{prefix}][ERROR] Failed to start: {str(e)}")
        return None


def install_dependencies(force: bool = False):
    # Backend dependencies
    req = BACKEND_DIR / "requirements.txt"
    # if req.exists():
<<<<<<< HEAD
    #     print("Checking/installing backend dependencies...")
    #     try:
    #         # Force using the current interpreter's pip to avoid pollution from other virtual environments
    #         run_blocking([sys.executable, "-m", "pip", "install", "--upgrade", "pip"], cwd=BACKEND_DIR)
    #         run_blocking([sys.executable, "-m", "pip", "install", "-r", str(req)] , cwd=BACKEND_DIR)
    #     except subprocess.CalledProcessError as e:
    #         print(f"[WARN] Failed to install backend dependencies: {e}")
    # else:
    #     print("[WARN] backend/requirements.txt not found, skipping backend dependency installation")
=======
    #     print("检查/安装后端依赖...")
    #     try:
    #         # 强制使用当前解释器对应的pip，避免其他虚拟环境污染
    #         run_blocking([sys.executable, "-m", "pip", "install", "--upgrade", "pip"], cwd=BACKEND_DIR)
    #         run_blocking([sys.executable, "-m", "pip", "install", "-r", str(req)] , cwd=BACKEND_DIR)
    #     except subprocess.CalledProcessError as e:
    #         print(f"[WARN] 安装后端依赖失败: {e}")
    # else:
    #     print("[WARN] 未找到 backend/requirements.txt，跳过后端依赖安装")
>>>>>>> f0cd8b0b74e9f5b3846fc9d7f32c83333950b8ca

    # Frontend dependencies
    nm = FRONTEND_DIR / "node_modules"
    need_install = force or (not nm.exists())
    if need_install:
        print("Installing frontend dependencies (npm install)...")
        try:
            npm_cmd = get_npm_cmd()
            run_blocking([npm_cmd, "install"], cwd=FRONTEND_DIR)
        except subprocess.CalledProcessError as e:
            print(f"[WARN] Failed to install frontend dependencies: {e}")
    else:
        print("Detected frontend/node_modules, skipping frontend installation. Use --install to force reinstall.")


def main():
    parser = argparse.ArgumentParser(description="One-click startup for backend and frontend")
    parser.add_argument("--install", action="store_true", help="Install backend/frontend dependencies before starting")
    parser.add_argument("--backend-only", action="store_true", help="Start backend service only")
    parser.add_argument("--frontend-only", action="store_true", help="Start frontend service only")
    args = parser.parse_args()

    print("==== One-Click Startup Script (Python) ====")
    print(f"Project root directory: {ROOT}")

    if args.install:
        install_dependencies(force=True)
    else:
        # 尝试最小安装（若缺失）
        install_dependencies(force=False)

    # Initialize process variables
    backend_proc = None
    frontend_proc = None
    exit_code = 0

    # Start backend (unless frontend-only is specified)
    if not args.frontend_only:
        backend_cmd = [sys.executable, "run.py"]
        backend_proc = start_process(backend_cmd, BACKEND_DIR, prefix="BACKEND")
        if not backend_proc:
            print("[ERROR] Backend startup failed, please check logs")
            return 1

    # Start frontend (unless backend-only is specified)
    if not args.backend_only:
        npm_cmd = get_npm_cmd()
        frontend_cmd = [npm_cmd, "run", "dev"]
        frontend_proc = start_process(frontend_cmd, FRONTEND_DIR, prefix="FRONTEND")
        if not frontend_proc:
            print("[ERROR] Frontend startup failed, please check logs")
            # If backend is already running, clean up
            if backend_proc and backend_proc.poll() is None:
                try:
                    print("[CLEANUP] Terminating backend process...")
                    backend_proc.terminate()
                    # Give the process some time to terminate
                    time.sleep(1)
                except Exception as e:
                    print(f"[ERROR] Failed to terminate backend process: {str(e)}")
            return 1

    print("\n==== Startup Information ====")
    if backend_proc:
        print("Backend API:    http://localhost:5000 (Health check: /api/health)")
    if frontend_proc:
        print("Frontend Web:   http://localhost:3000")
    if backend_proc and frontend_proc:
        print("API Proxy(Next): http://localhost:3000/api -> http://localhost:5000/api")
    print("Press Ctrl+C to end and clean up child processes.\n")

    try:
        while True:
            time.sleep(1)
            # Check backend process status
            if backend_proc and backend_proc.poll() is not None:
                print(f"[BACKEND] Process exited, code: {backend_proc.returncode}")
                if backend_proc.returncode != 0:
                    print("[BACKEND] Backend abnormally exited, please check logs")
                    exit_code = 1
                break
            # Check frontend process status
            if frontend_proc and frontend_proc.poll() is not None:
                print(f"[FRONTEND] Process exited, code: {frontend_proc.returncode}")
                if frontend_proc.returncode != 0:
                    print("[FRONTEND] Frontend abnormally exited, please check logs")
                    exit_code = 1
                break
    except KeyboardInterrupt:
        print("\nReceived interrupt signal, stopping child processes...")
    finally:
        # End child processes
        for name, proc in (("BACKEND", backend_proc), ("FRONTEND", frontend_proc)):
            if proc and proc.poll() is None:
                try:
                    print(f"[{name}] Stopping process...")
                    if os.name == "nt":
                        proc.terminate()
                    else:
                        os.kill(proc.pid, signal.SIGTERM)
                except Exception as e:
                    print(f"[{name}] Failed to stop process: {str(e)}")
        # Wait for processes to end
        time.sleep(1)
        print("Attempted to end all child processes.")
    
    return 0


if __name__ == "__main__":
    exit_code = main()
    sys.exit(exit_code if exit_code is not None else 0)